import React, { useState, useEffect, useMemo, useRef } from 'react'; import { Home, MapPin, Settings, Search, Bell, Activity, ClipboardList, Calendar, CheckCircle, Clock, AlertTriangle, Wind, Droplet, ShieldAlert, Users, FileText, Database, DownloadCloud, Check, RefreshCw, Info, Printer, ShieldCheck, ShieldX, Map as MapIcon, ListFilter, ArrowRight, Crosshair, Edit, Target, PlayCircle, Eye, User, AlertCircle, Home as HomeIcon, BellRing, Bug, Lightbulb, Timer, Flame, UploadCloud, ExternalLink, FolderOpen, BarChart2, ChevronUp, Filter, Camera, Menu, X, Download } from 'lucide-react'; const parseOnsetObject = (dateStr) => { if (!dateStr || dateStr === 'ไม่ระบุวันที่' || dateStr === '-') return null; if (dateStr instanceof Date) return dateStr; if (typeof dateStr === 'string') { const timestampDate = new Date(dateStr); if (!isNaN(timestampDate) && dateStr.includes('T')) return timestampDate; if (dateStr.includes('-')) { const d = new Date(dateStr); if (!isNaN(d)) return d; } if (dateStr.includes('/')) { const parts = dateStr.split('/'); if (parts.length === 3) { let year = parseInt(parts[2]); if (year > 2500) year -= 543; return new Date(year, parseInt(parts[1]) - 1, parseInt(parts[0])); } } const fallbackDate = new Date(dateStr); if (!isNaN(fallbackDate)) return fallbackDate; } return null; }; const formatThaiDate = (dateInput) => { if (!dateInput) return '-'; const d = parseOnsetObject(dateInput); if (!d || isNaN(d)) return typeof dateInput === 'string' ? dateInput : '-'; const day = d.getDate(); const monthNames = ["ม.ค.", "ก.พ.", "มี.ค.", "เม.ย.", "พ.ค.", "มิ.ย.", "ก.ค.", "ส.ค.", "ก.ย.", "ต.ค.", "พ.ย.", "ธ.ค."]; return `${day} ${monthNames[d.getMonth()]} ${d.getFullYear() + 543}`; }; const formatDateTimeThai = (dateString) => { if (!dateString) return '-'; const d = new Date(dateString); if (isNaN(d)) return '-'; const day = d.getDate(); const monthNames = ["ม.ค.", "ก.พ.", "มี.ค.", "เม.ย.", "พ.ค.", "มิ.ย.", "ก.ค.", "ส.ค.", "ก.ย.", "ต.ค.", "พ.ย.", "ธ.ค."]; const month = monthNames[d.getMonth()]; const year = d.getFullYear() + 543; const hours = String(d.getHours()).padStart(2, '0'); const mins = String(d.getMinutes()).padStart(2, '0'); return `${day} ${month} ${year} เวลา ${hours}:${mins} น.`; }; const GOOGLE_FORM_URL = "https://docs.google.com/forms/d/e/1FAIpQLSc4ggD3F1yHaKRvAM38WeTt5dw1_86efa-HsHknxeiTRlL-WQ/viewform"; const GOOGLE_SHEET_URL = "https://docs.google.com/spreadsheets/d/1SV9KyyKne4quhDzBj5k0LGC6A5TiOlXmVVtyEgYWqcw/edit?resourcekey=&gid=67922063#gid=67922063"; const GOOGLE_SCRIPT_URL = 'https://script.google.com/macros/s/AKfycby5m3Jr_lswIjob7Zprp9K2w449RJFxPMa3gjknMtB2849oiCB0_auejMqj3yW-Fh-H/exec'; const App = () => { const [activeMenu, setActiveMenu] = useState('patients'); const [selectedPatient, setSelectedPatient] = useState(null); const [activePhase, setActivePhase] = useState('3h1'); const [fetchedCases, setFetchedCases] = useState([]); const [isLoadingCases, setIsLoadingCases] = useState(true); const [isSubmitting, setIsSubmitting] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const [selectedYear, setSelectedYear] = useState('latest'); const [formData, setFormData] = useState({}); const [isLoadingHistory, setIsLoadingHistory] = useState(false); const [editMode, setEditMode] = useState(false); const [trackingHistory, setTrackingHistory] = useState({}); const [expandedKpi, setExpandedKpi] = useState(null); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isDownloadingImage, setIsDownloadingImage] = useState(false); const summaryRef = useRef(null); const initialSteps = [ { id: '3h1', label: 'Day 0 (รับแจ้ง)', desc: 'รายงาน รพ.สต.', targetDays: 0 }, { id: '3h2', label: 'Day 0 (ลงพื้นที่)', desc: 'สเปรย์/สำรวจ', targetDays: 0 }, { id: '1d', label: 'Day 1', desc: 'พ่นครั้งที่ 1', targetDays: 1 }, { id: '3d', label: 'Day 3', desc: 'พ่นครั้งที่ 2', targetDays: 3 }, { id: '7d', label: 'Day 7', desc: 'พ่นครั้งที่ 3', targetDays: 7 }, { id: '14d', label: 'Day 14', desc: 'สำรวจลูกน้ำ', targetDays: 14 }, { id: '21d', label: 'Day 21', desc: 'สำรวจลูกน้ำ', targetDays: 21 }, { id: '28d', label: 'Day 28', desc: 'ประเมิน 28 วัน', targetDays: 28 }, { id: 'summary', label: 'สรุปภาพรวม', desc: 'รายงานปิดเคส', targetDays: 28 }, ]; // 🌟 อัปเดตแนวทางปฏิบัติตามมาตรฐานกระทรวงสาธารณสุขเป๊ะๆ const phaseGuidelines = { '3h1': 'ภายใน 3 ชั่วโมงแรก สถานบริการสาธารณสุขรายงานโรคให้ รพ.สต. หรือศูนย์บริการสาธารณสุขในพื้นที่ทราบ', '3h2': 'ภายใน 3 ชั่วโมงหลังได้รับรายงาน รพ.สต. หรือศูนย์บริการสาธารณสุขในพื้นที่ สอบสวนโรค ทำลายแหล่งเพาะพันธุ์ยุงลาย ฉีดสเปรย์กระป๋องกำจัดยุงในบ้านผู้ป่วย และจ่ายยาทากันยุงให้ผู้ป่วยรวมถึงกลุ่มเสี่ยง', '1d': '1 วันหลังได้รับรายงาน อสม./อสส. สำรวจและทำลายแหล่งเพาะพันธุ์ยุงลาย และ อปท. พ่นสารเคมีกำจัดยุง ที่บ้านผู้ป่วยและบ้านที่อยู่ในรัศมี 100 เมตร ทั้งในและนอกบ้าน และจุดที่สงสัยเป็นแหล่งโรค', '3d': '3 วันหลังได้รับรายงาน อปท. พ่นสารเคมีกำจัดยุง ที่บ้านผู้ป่วยและบ้านที่อยู่ในรัศมี 100 เมตร ทั้งในและนอกบ้าน และจุดที่สงสัยเป็นแหล่งโรค', '7d': '7 วันหลังได้รับรายงาน อสม./อสส. สำรวจและทำลายแหล่งเพาะพันธุ์ยุงลาย และ อปท. พ่นสารเคมีกำจัดยุง รัศมี 100 เมตร (เป้าหมาย HI และ CI ในรัศมี 100 เมตรจากบ้านผู้ป่วยต้องเป็นศูนย์)', '14d': '14 วันหลังได้รับรายงาน อสม./อสส. สำรวจและทำลายแหล่งเพาะพันธุ์ยุงลายทั้งหมู่บ้าน เฝ้าระวังและติดตามผู้ป่วยรุ่นที่ 2 (เป้าหมาย HI ในหมู่บ้านไม่เกินร้อยละ 5)', '21d': '21 วันหลังได้รับรายงาน อสม./อสส. สำรวจและทำลายแหล่งเพาะพันธุ์ยุงทั้งหมู่บ้าน เฝ้าระวังและติดตามผู้ป่วยรุ่นที่ 2 (เป้าหมาย HI หมู่บ้านไม่เกิน 5%, CI สถานพยาบาล/โรงเรียน เป็น 0, CI โรงธรรม/โรงแรม/โรงงาน/สถานที่ราชการ ไม่เกิน 5%)', '28d': '28 วันหลังได้รับรายงาน คงมาตรการสำรวจและทำลายแหล่งเพาะพันธุ์ยุงในชุมชน ทุก 7 วัน โดยให้ชุมชนมีส่วนร่วม และเฝ้าระวัง ติดตามผู้ป่วยรุ่นที่ 2 (หลังจาก 28 วัน ทบทวนและถอดบทเรียนการดำเนินมาตรการฯ)', 'summary': 'สรุปผลการดำเนินงานตั้งแต่ Day 0 ถึง Day 28 เพื่อใช้ประกอบการรายงานสอบสวนโรคเบื้องต้น' }; const [steps, setSteps] = useState(initialSteps.map(s => ({ ...s, status: 'pending', date: '-' }))); useEffect(() => { const fetchData = async () => { try { setIsLoadingCases(true); const csvPromise = fetch('https://docs.google.com/spreadsheets/d/e/2PACX-1vQI5kThGnr-ba-HnQQRD7iSSZkloNQf3CVQ5omvq0sgzd1_OVXoGU9Y4GJm5d9Ae7Nbvh_E29VTAEY_/pub?output=csv').then(res => res.text()); const trackingPromise = fetch(`${GOOGLE_SCRIPT_URL}?action=getAllWithData`).then(res => res.json()).catch(() => ({ status: 'error', data: [] })); const [csvText, trackingResult] = await Promise.all([csvPromise, trackingPromise]); const trackData = {}; if (trackingResult?.status === 'success' && trackingResult.data) { trackingResult.data.forEach(row => { if (!trackData[row.hn]) trackData[row.hn] = {}; trackData[row.hn][row.phase] = row; }); } setTrackingHistory(trackData); const lines = csvText.split('\n').filter(line => line.trim() !== ''); if (lines.length === 0) throw new Error("CSV is empty"); const headers = lines[0].split(',').map(h => h.trim().replace(/^"|"$/g, '')); const hnIdx = headers.findIndex(h => h.includes('HN') || h.includes('รหัส')); const ageIdx = headers.findIndex(h => h.includes('อายุ')); const dateIdx = headers.findIndex(h => h.includes('เริ่มป่วย') || h.includes('Onset')); const subdistrictIdx = headers.findIndex(h => h.includes('ตำบล')); const villageIdx = headers.findIndex(h => h.includes('หมู่บ้าน') || h.includes('หมู่ที่')); const addressIdx = headers.findIndex(h => h.includes('บ้านเลขที่') || h.includes('ที่อยู่')); const latIdx = headers.findIndex(h => h.toLowerCase() === 'lat' || h.includes('ละติจูด')); const lngIdx = headers.findIndex(h => h.toLowerCase() === 'lng' || h.includes('ลองติจูด')); const parsedData = lines.slice(1).map((line, idx) => { const values = line.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/).map(v => v.replace(/^"|"$/g, '').trim()); const rawDate = values[dateIdx] || ''; const onsetDateObj = parseOnsetObject(rawDate); let daysPassed = 0; let endDateStr = '-'; if (onsetDateObj) { daysPassed = Math.floor((new Date().getTime() - onsetDateObj.getTime()) / (1000 * 60 * 60 * 24)); const endDate = new Date(onsetDateObj); endDate.setDate(endDate.getDate() + 28); endDateStr = formatThaiDate(endDate); } let caseYear = 'ไม่ระบุ'; if (rawDate) { const matchY = rawDate.match(/\b(25\d{2}|20\d{2})\b/); if (matchY) caseYear = matchY[1]; } let rawHN = values[hnIdx] || ''; if (!rawHN) { const shortYear = caseYear !== 'ไม่ระบุ' ? caseYear.toString().slice(-2) : new Date().getFullYear().toString().slice(-2); rawHN = `${shortYear}-${String(idx+1).padStart(3, '0')}`; } const patientPhases = Object.keys(trackData[rawHN] || {}); let realStatus = 'รอบันทึกข้อมูล'; if (patientPhases.length > 0) { realStatus = (patientPhases.includes('28d') || patientPhases.includes('summary')) ? 'เสร็จสิ้นการเฝ้าระวัง' : 'ระหว่างการเฝ้าระวัง'; } return { id: `${rawHN}-${idx}`, displayHn: rawHN, age: values[ageIdx] || '0', date: formatThaiDate(onsetDateObj), rawDate: rawDate, onsetDateObj: onsetDateObj, endDate: endDateStr, daysSinceOnset: Math.max(0, daysPassed), address: `${values[addressIdx] || ''} ${values[villageIdx] || ''} ต.${values[subdistrictIdx] || ''}`, village: values[villageIdx] || '', subdistrict: values[subdistrictIdx] || '', lat: parseFloat(values[latIdx]) || (15.93 + (Math.random() - 0.5) * 0.05), lng: parseFloat(values[lngIdx]) || (104.91 + (Math.random() - 0.5) * 0.05), status: realStatus, year: caseYear, completedPhases: patientPhases }; }); setFetchedCases(parsedData); } catch (err) { console.warn("Error fetching Data:", err.message); } finally { setIsLoadingCases(false); } }; fetchData(); }, []); const availableYears = useMemo(() => [...new Set(fetchedCases.map(p => p.year))].filter(y => y !== 'ไม่ระบุ').sort().reverse(), [fetchedCases]); useEffect(() => { if (selectedYear === 'latest' && availableYears.length > 0) { setSelectedYear(availableYears[0]); } }, [availableYears, selectedYear]); const kpiStats = useMemo(() => { const yearCases = fetchedCases.filter(p => selectedYear === 'all' || selectedYear === 'latest' ? true : p.year === selectedYear); const totalCases = yearCases.length; // KPI 2: Complete Tracking 28 Days const past28DaysCases = yearCases.filter(c => c.daysSinceOnset > 28); let completeTracking = 0; past28DaysCases.forEach(c => { const p = c.completedPhases; if (p.includes('1d') && p.includes('3d') && p.includes('7d') && (p.includes('28d') || p.includes('summary'))) { completeTracking++; } }); const kpi2 = past28DaysCases.length === 0 ? 0 : ((completeTracking / past28DaysCases.length) * 100); // KPI 3: No new cases in 28 days let noNewCasesCount = 0; past28DaysCases.forEach(c => { const vCases = yearCases.filter(other => other.subdistrict === c.subdistrict && other.village === c.village && other.id !== c.id); let hasNewWithin28 = false; vCases.forEach(other => { if (c.onsetDateObj && other.onsetDateObj) { const diffDays = (other.onsetDateObj.getTime() - c.onsetDateObj.getTime()) / (1000 * 60 * 60 * 24); if (diffDays > 0 && diffDays <= 28) hasNewWithin28 = true; } }); if (!hasNewWithin28) noNewCasesCount++; }); const kpi3 = past28DaysCases.length === 0 ? 0 : ((noNewCasesCount / past28DaysCases.length) * 100); // KPI 4: Gen 2 Rate (Day 15-28) let gen2CasesCount = 0; yearCases.forEach(c => { const vCases = yearCases.filter(other => other.subdistrict === c.subdistrict && other.village === c.village && other.id !== c.id); let isGen2 = false; vCases.forEach(other => { if (c.onsetDateObj && other.onsetDateObj) { const diffDays = (c.onsetDateObj.getTime() - other.onsetDateObj.getTime()) / (1000 * 60 * 60 * 24); if (diffDays >= 15 && diffDays <= 28) isGen2 = true; } }); if (isGen2) gen2CasesCount++; }); const kpi4 = totalCases === 0 ? 0 : ((gen2CasesCount / totalCases) * 100); // KPI 5: Recurrent Larvae Rate let recurrentEarlyCases = 0; let surveyedEarlyCases = 0; yearCases.forEach(c => { const history = trackingHistory[c.displayHn] || {}; const earlyPhases = ['3h2', '1d', '3d', '7d']; let allHouses = []; let hasRecurrent = false; let surveyCount = 0; earlyPhases.forEach(p => { const hData = history[p]?.formData?.housesFound100m; if (history[p]) surveyCount++; if (hData) { const houses = hData.split(/[\s,]+/).map(s => s.trim()).filter(s => s.length > 0); houses.forEach(h => { if (allHouses.includes(h)) hasRecurrent = true; allHouses.push(h); }); } }); if (surveyCount >= 2) { surveyedEarlyCases++; if (hasRecurrent) recurrentEarlyCases++; } }); const kpi5 = surveyedEarlyCases === 0 ? 0 : ((recurrentEarlyCases / surveyedEarlyCases) * 100); return { kpi2: { val: kpi2.toFixed(1), num: completeTracking, den: past28DaysCases.length, unit: 'ราย' }, kpi3: { val: kpi3.toFixed(1), num: noNewCasesCount, den: past28DaysCases.length, unit: 'ราย' }, kpi4: { val: kpi4.toFixed(1), num: gen2CasesCount, den: totalCases, unit: 'ราย' }, kpi5: { val: kpi5.toFixed(1), num: recurrentEarlyCases, den: surveyedEarlyCases, unit: 'พื้นที่' }, }; }, [fetchedCases, trackingHistory, selectedYear]); const filteredPatients = useMemo(() => { return fetchedCases.filter(p => { const matchSearch = p.displayHn.toLowerCase().includes(searchTerm.toLowerCase()) || p.address.includes(searchTerm); const matchYear = (selectedYear === 'all' || selectedYear === 'latest') ? true : p.year === selectedYear; return matchSearch && matchYear; }); }, [fetchedCases, searchTerm, selectedYear]); const systemAlerts = useMemo(() => { const alerts = { overdue: [], dueToday: [], kpiFailed: [], outbreaks: [] }; const currentThaiYear = (new Date().getFullYear() + 543).toString(); const subdistrictStats = {}; const currentYearCases = fetchedCases .filter(c => c.year === currentThaiYear) .sort((a, b) => (a.onsetDateObj?.getTime() || 0) - (b.onsetDateObj?.getTime() || 0)); currentYearCases.forEach(c => { const vKey = `${c.subdistrict}|${c.village}`; if (!subdistrictStats[vKey]) subdistrictStats[vKey] = { cases: [], duration: 0 }; const stats = subdistrictStats[vKey]; const lastCase = stats.cases[stats.cases.length - 1]; if (lastCase && c.onsetDateObj && lastCase.onsetDateObj) { const gap = Math.floor((c.onsetDateObj.getTime() - lastCase.onsetDateObj.getTime()) / (1000 * 60 * 60 * 24)); if (gap <= 28) { stats.duration += gap; if (stats.duration > 28 && !alerts.outbreaks.includes(vKey.replace('|', ' ต.'))) { alerts.outbreaks.push(vKey.replace('|', ' ต.')); } } else { stats.duration = 0; } } stats.cases.push(c); if (c.status !== 'เสร็จสิ้นการเฝ้าระวัง') { const dso = c.daysSinceOnset; const nextRequiredPhase = initialSteps.find(s => s.id !== 'summary' && s.targetDays <= dso && !c.completedPhases.includes(s.id)); if (nextRequiredPhase) { const expectedDays = nextRequiredPhase.targetDays; const isOverdue = dso > expectedDays; const alertObj = { hn: c.displayHn, village: c.village, phaseName: nextRequiredPhase.label, daysPassed: dso, expectedDay: expectedDays, patient: c }; if (isOverdue) alerts.overdue.push(alertObj); else if (dso === expectedDays) alerts.dueToday.push(alertObj); } } const pHistory = trackingHistory[c.displayHn]; if (pHistory) { if (pHistory['7d']) { const fd = pHistory['7d'].formData; const hi100 = (fd?.survey100mFound / fd?.survey100mTotal) * 100 || 0; if (hi100 > 0) alerts.kpiFailed.push({ hn: c.displayHn, village: c.village, issue: `Day 7: HI 100ม. = ${hi100.toFixed(1)}% (เป้า 0%)` }); } if (pHistory['14d']) { const fd = pHistory['14d'].formData; const hiVillage = (fd?.surveyCommFound / fd?.surveyCommTotal) * 100 || 0; if (hiVillage > 5) alerts.kpiFailed.push({ hn: c.displayHn, village: c.village, issue: `Day 14: HI หมู่บ้าน = ${hiVillage.toFixed(1)}% (เป้า < 5%)` }); } } }); return alerts; }, [fetchedCases, trackingHistory]); useEffect(() => { if (activeMenu !== 'map' || isLoadingCases) return; let mapInstance = null; const initMap = async () => { if (!document.getElementById('leaflet-css')) { const link = document.createElement('link'); link.id = 'leaflet-css'; link.rel = 'stylesheet'; link.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'; document.head.appendChild(link); } if (!window.L) { const script = document.createElement('script'); script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js'; document.head.appendChild(script); await new Promise(resolve => script.onload = resolve); } const container = document.getElementById('real-leaflet-map'); if (container && !container._leaflet_id) { mapInstance = window.L.map('real-leaflet-map'); window.L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { attribution: 'Tiles © Esri' }).addTo(mapInstance); const bounds = window.L.latLngBounds([]); filteredPatients.forEach(patient => { const latLng = [patient.lat, patient.lng]; bounds.extend(latLng); const isCompleted = patient.status === 'เสร็จสิ้นการเฝ้าระวัง'; const isPending = patient.status === 'รอบันทึกข้อมูล'; const circleColor = isCompleted ? '#10B981' : (isPending ? '#EF4444' : '#F59E0B'); const markerColor = isCompleted ? '#059669' : (isPending ? '#B91C1C' : '#D97706'); window.L.circle(latLng, { color: circleColor, fillColor: circleColor, fillOpacity: 0.3, weight: 2, radius: 100 }).addTo(mapInstance); const marker = window.L.circleMarker(latLng, { radius: 5, color: '#ffffff', weight: 2, fillColor: markerColor, fillOpacity: 1 }).addTo(mapInstance); const popupContent = `
รหัสผู้ป่วย: ${patient.displayHn}
อายุ: ${patient.age} ปี
พิกัด: หมู่บ้าน${patient.village} ต.${patient.subdistrict}
วันเริ่มป่วย: ${patient.date}
วันสิ้นสุด: ${patient.endDate}
${patient.status}
`; marker.bindPopup(popupContent); if (selectedPatient && patient.id === selectedPatient.id) { setTimeout(() => marker.openPopup(), 500); } }); if (selectedPatient) { mapInstance.setView([selectedPatient.lat, selectedPatient.lng], 17); } else if (bounds.isValid()) { mapInstance.fitBounds(bounds, { padding: [20, 20], maxZoom: 16 }); } else { mapInstance.setView([15.9320, 104.9150], 13); } } }; initMap(); return () => { if (mapInstance) mapInstance.remove(); }; }, [activeMenu, fetchedCases, isLoadingCases, searchTerm, selectedYear, selectedPatient, filteredPatients]); const handleDownloadSummaryImage = async () => { if (!summaryRef.current) return; setIsDownloadingImage(true); try { if (!window.html2canvas) { const script = document.createElement('script'); script.src = 'https://html2canvas.hertzen.com/dist/html2canvas.min.js'; document.head.appendChild(script); await new Promise(resolve => script.onload = resolve); } const canvas = await window.html2canvas(summaryRef.current, { scale: 2, useCORS: true, backgroundColor: '#f1f5f9' }); const image = canvas.toDataURL("image/png"); const link = document.createElement('a'); link.href = image; link.download = `สรุปเคส_${selectedPatient.displayHn}.png`; link.click(); } catch (error) { console.error(error); alert("เกิดข้อผิดพลาด ไม่สามารถสร้างไฟล์รูปภาพได้"); } finally { setIsDownloadingImage(false); } }; const handleMenuClick = (menu) => { setActiveMenu(menu); setIsMobileMenuOpen(false); }; const handleUpdateStatus = async (stepId) => { setIsSubmitting(true); const stepFormData = formData[stepId] || {}; if (!stepFormData.visitorName) { stepFormData.visitorName = "นายธนากร โสเสมอ"; } const payload = { hn: selectedPatient.displayHn, phase: stepId, status: 'completed', formData: stepFormData }; try { await fetch(GOOGLE_SCRIPT_URL, { method: 'POST', mode: 'no-cors', body: JSON.stringify(payload) }); const updatedSteps = steps.map((s, i) => { if (s.id === stepId) return { ...s, status: 'completed', date: formatThaiDate(new Date()) }; if (!editMode && i === steps.findIndex(x => x.id === stepId) + 1 && s.status === 'pending') { setActivePhase(s.id); return { ...s, status: 'active' }; } return s; }); setSteps(updatedSteps); setEditMode(false); const newStatus = (stepId === '28d' || stepId === 'summary') ? 'เสร็จสิ้นการเฝ้าระวัง' : 'ระหว่างการเฝ้าระวัง'; setFetchedCases(prev => prev.map(p => p.displayHn === selectedPatient.displayHn ? { ...p, status: newStatus, completedPhases: [...p.completedPhases, stepId] } : p)); setTrackingHistory(prev => ({ ...prev, [selectedPatient.displayHn]: { ...(prev[selectedPatient.displayHn] || {}), [stepId]: { phase: stepId, formData: stepFormData } } })); } catch (err) { console.warn(err); } finally { setIsSubmitting(false); } }; const handleSelectPatient = async (patient) => { setSelectedPatient(patient); const baseSteps = initialSteps.map(s => ({ ...s, status: 'pending', date: '-' })); baseSteps[0].status = 'active'; setSteps(baseSteps); setActivePhase('3h1'); handleMenuClick('tracking'); setFormData({}); setIsLoadingHistory(true); setEditMode(false); try { const res = await fetch(`${GOOGLE_SCRIPT_URL}?hn=${encodeURIComponent(patient.displayHn)}`); const result = await res.json(); if (result?.status === 'success' && result.data?.length > 0) { let updatedSteps = [...baseSteps]; let newFormDataByPhase = {}; let latestPhase = '3h1'; result.data.forEach(item => { const idx = updatedSteps.findIndex(s => s.id === item.phase); if (idx !== -1) { updatedSteps[idx].status = 'completed'; updatedSteps[idx].date = formatThaiDate(item.timestamp); if (idx + 1 < updatedSteps.length) { updatedSteps[idx + 1].status = 'active'; latestPhase = updatedSteps[idx + 1].id; } } newFormDataByPhase[item.phase] = item.formData; }); setSteps(updatedSteps); setFormData(newFormDataByPhase); setActivePhase(latestPhase); } } catch (err) { console.warn(err); } finally { setIsLoadingHistory(false); } }; const handleInputChange = (e) => { const { name, value, type, checked } = e.target; setFormData(prev => ({ ...prev, [activePhase]: { ...(prev[activePhase] || {}), [name]: type === 'checkbox' ? checked : value } })); }; const calcPercent = (f, t) => (!t || t == 0) ? "0.00" : ((f / t) * 100).toFixed(2); const renderLarvaeSurveyForm = (isLocked, phaseTarget = "") => { const data = formData[activePhase] || {}; const hi100 = parseFloat(calcPercent(data.survey100mFound, data.survey100mTotal)); const hiVillage = parseFloat(calcPercent(data.surveyCommFound, data.surveyCommTotal)); const ciSchVal = parseFloat(calcPercent(data.schoolFound, data.schoolTotal)); const ciOthVal = parseFloat(calcPercent(data.otherPlaceFound, data.otherPlaceTotal)); const parseHouses = (str) => { if (!str) return []; return str.split(/[\s,]+/).map(s => s.trim()).filter(s => s.length > 0); }; const phasesOrder = ['3h2', '1d', '3d', '7d', '14d', '21d']; const currentIdx = phasesOrder.indexOf(activePhase); let previousHouses = []; if (currentIdx > 0) { for (let i = 0; i < currentIdx; i++) { const pData = formData[phasesOrder[i]]; if (pData && pData.housesFound100m) { previousHouses = [...previousHouses, ...parseHouses(pData.housesFound100m)]; } } } const currentHouses = parseHouses(data.housesFound100m); const repeatedHouses = currentHouses.filter(h => previousHouses.includes(h)); const uniqueRepeats = [...new Set(repeatedHouses)]; let targetAlert = null; let targetAlertCI = null; if (['1d', '3d', '7d'].includes(activePhase) && hi100 > 0) { targetAlert =
ดัชนีลูกน้ำยังไม่ถึงเกณฑ์: เป้าหมาย HI รัศมี 100ม. ต้องเป็น 0%
; } else if (['14d', '21d'].includes(activePhase) && hiVillage > 5) { targetAlert =
ดัชนีลูกน้ำยังไม่ถึงเกณฑ์: เป้าหมาย HI ในหมู่บ้านต้องไม่เกิน 5%
; } if (activePhase === '21d') { const failsSchool = ciSchVal > 0; const failsOther = ciOthVal > 5; if (failsSchool || failsOther) { targetAlertCI =
ดัชนีลูกน้ำสถานที่สำคัญไม่ผ่านเกณฑ์:
{failsSchool && "• สถานพยาบาล/โรงเรียน ต้องเป็น 0% "}{failsOther && "• สถานที่อื่นๆ ต้องไม่เกิน 5%"}
; } } return (

แบบบันทึกการสำรวจลูกน้ำยุงลาย (HI/CI)

1. การสำรวจระดับครัวเรือน (ค่า HI) {phaseTarget && ({phaseTarget})}
📍 รัศมี 100 เมตร
0 && ['1d', '3d', '7d'].includes(activePhase) ? 'bg-rose-50 text-rose-700 border-rose-200' : 'bg-emerald-50 text-emerald-700 border-emerald-200'}`}>{hi100.toFixed(2)}%
{uniqueRepeats.length > 0 && (
แจ้งเตือน: พบลูกน้ำซ้ำซากในบ้านเลขที่ {uniqueRepeats.join(', ')} เคยพบลูกน้ำในรอบก่อนหน้านี้แล้ว กรุณาเน้นย้ำและลงพื้นที่กำจัดลูกน้ำยุงลายซ้ำโดยด่วน!
)}
🏡 ทั้งหมู่บ้าน
5 && ['14d', '21d'].includes(activePhase) ? 'bg-amber-50 text-amber-700 border-amber-200' : 'bg-emerald-50 text-emerald-700 border-emerald-200'}`}>{hiVillage.toFixed(2)}%
{targetAlert}
2. การสำรวจสถานที่สำคัญ (ค่า CI)
0 && activePhase === '21d' ? 'bg-rose-50 text-rose-700 border-rose-200' : 'bg-white text-slate-700 border-slate-300'}`}>{calcPercent(data.schoolFound, data.schoolTotal)}%
5 && activePhase === '21d' ? 'bg-rose-50 text-rose-700 border-rose-200' : 'bg-white text-slate-700 border-slate-300'}`}>{calcPercent(data.otherPlaceFound, data.otherPlaceTotal)}%
{targetAlertCI}
); }; const renderAlertCenter = () => { if (isLoadingCases) return null; const hasAlerts = systemAlerts.overdue.length > 0 || systemAlerts.dueToday.length > 0 || systemAlerts.kpiFailed.length > 0 || systemAlerts.outbreaks.length > 0; if (!hasAlerts) { return (

สถานการณ์ปกติ (ข้อมูลปีปัจจุบัน)

เยี่ยมมาก! ไม่มีกิจกรรมค้างดำเนินการ และพื้นที่ระบาดอยู่ในเกณฑ์ควบคุมได้

); } return (

แจ้งเตือนภารกิจปฏิบัติการ (เฉพาะปีปัจจุบัน)

{systemAlerts.overdue.length + systemAlerts.kpiFailed.length + systemAlerts.outbreaks.length} รายการที่ต้องติดตาม

เลยกำหนดลงพื้นที่ ({systemAlerts.overdue.length})

{systemAlerts.overdue.length === 0 ?

ไม่มีภารกิจค้าง

: systemAlerts.overdue.map((a, i) => (
handleSelectPatient(a.patient)}>
{a.hn} ช้าไป {a.daysPassed - a.expectedDay} วัน
{a.village} รอ {a.phaseName}
)) }

ภารกิจวันนี้ ({systemAlerts.dueToday.length})

{systemAlerts.dueToday.length === 0 ?

ไม่มีภารกิจวันนี้

: systemAlerts.dueToday.map((a, i) => (
handleSelectPatient(a.patient)}>
{a.hn} {a.phaseName}
{a.village}
)) }

เฝ้าระวังพื้นที่ระบาด

{systemAlerts.outbreaks.map((v, i) => (
🚨 ระบาดต่อเนื่อง ({'>'} 28 วัน) {v}
))} {systemAlerts.kpiFailed.map((k, i) => (
{k.hn} ({k.village}) {k.issue}
))} {systemAlerts.outbreaks.length === 0 && systemAlerts.kpiFailed.length === 0 &&

ทุกพื้นที่อยู่ในเกณฑ์ปกติ

}
); }; const renderPatientList = () => { return (
{renderAlertCenter()}

ทะเบียนผู้ป่วยโรคไข้เลือดออก

setSearchTerm(e.target.value)} className="bg-transparent border-none outline-none py-2.5 text-sm w-full md:w-56 text-slate-700 font-medium placeholder-slate-400" />
{isLoadingCases ? ( ) : filteredPatients.length === 0 ? ( ) : filteredPatients.map((p) => { const isOverdue = systemAlerts.overdue.some(a => a.hn === p.displayHn); const isDueToday = systemAlerts.dueToday.some(a => a.hn === p.displayHn); let statusBadgeClass = "bg-rose-50 text-rose-700 border-rose-200"; if (p.status === 'ระหว่างการเฝ้าระวัง') statusBadgeClass = "bg-amber-50 text-amber-700 border-amber-200"; else if (p.status === 'เสร็จสิ้นการเฝ้าระวัง') statusBadgeClass = "bg-emerald-50 text-emerald-700 border-emerald-200"; return ( ); }) }
รหัสผู้ป่วย / อายุ หมู่บ้าน/ตำบล วันที่เริ่มป่วย (Onset) ระยะเวลาเฝ้าระวัง สถานะเคส จัดการ

กำลังโหลดข้อมูลผู้ป่วย...

ไม่พบข้อมูลผู้ป่วยที่ค้นหา
{p.displayHn}
อายุ {p.age} ปี
{p.village}
ต.{p.subdistrict}
{p.date}
{p.daysSinceOnset <= 28 ? `เป้า 28 วัน: ${p.endDate}` : `ปิดเคสเมื่อ: ${p.endDate}`}
{p.daysSinceOnset <= 28 ? ( Day {p.daysSinceOnset} / 28 ) : ( สิ้นสุดเฝ้าระวัง )} {p.status !== 'เสร็จสิ้นการเฝ้าระวัง' && p.daysSinceOnset <= 28 && (isOverdue || isDueToday) && (
{isOverdue && เกินกำหนด} {isDueToday && ต้องทำวันนี้}
)}
{p.status === 'เสร็จสิ้นการเฝ้าระวัง' && } {p.status === 'ระหว่างการเฝ้าระวัง' && } {p.status === 'รอบันทึกข้อมูล' && } {p.status}
); }; const renderMap = () => (

แผนที่แสดงพิกัดผู้ป่วย

แสดงพิกัดที่อยู่อาศัยและวงจรเฝ้าระวัง 100 เมตร (ดาวเทียม)

{selectedPatient && ( )}
รัศมี 100 เมตร

อ้างอิงพ่นหมอกควันและกำจัดแหล่งเพาะพันธุ์

); const renderTrackingView = () => { if (!selectedPatient) return null; const isLocked = steps.find(s => s.id === activePhase)?.status === 'completed' && !editMode; const currentData = formData[activePhase] || {}; const activeStepObj = initialSteps.find(s => s.id === activePhase); const targetDays = activeStepObj ? activeStepObj.targetDays : 0; const isPhaseOverdue = selectedPatient.daysSinceOnset > targetDays && !isLocked && activePhase !== 'summary'; const suggestionText = phaseGuidelines[activePhase]; return (

รหัส: {selectedPatient.displayHn}

อายุผู้ป่วย

{selectedPatient.age} ปี

พิกัดพื้นที่เกิดโรค

บ้าน{selectedPatient.village} ต.{selectedPatient.subdistrict}

วันเริ่มป่วย (Day 0)

{selectedPatient.date}

ระยะเวลาผ่านไป

{selectedPatient.daysSinceOnset <= 28 ? (

Day {selectedPatient.daysSinceOnset}

เป้าหมาย 28 วัน

) : (
ปิดเคสแล้ว เกิน 28 วันแล้ว
)}
{selectedPatient.daysSinceOnset <= 7 && (

ช่วงวิกฤต 7 วันแรก (Critical Transmission Period)

ผู้ป่วยยังอยู่ในระยะแพร่เชื้อ โปรดเร่งทำลายแหล่งเพาะพันธุ์และพ่นสารเคมีควบคุมโรคตามมาตรการ 1-3-7 ทันที เพื่อกำจัดยุงตัวเต็มวัยที่มีเชื้อก่อนที่จะบินไปกัดและแพร่เชื้อสู่ผู้อื่น

)} {selectedPatient.daysSinceOnset > 7 && selectedPatient.daysSinceOnset <= 14 && (

ช่วงเฝ้าระวังผู้ป่วยรายใหม่ (Secondary Gen Period)

เข้าสู่ช่วงสัปดาห์ที่ 2 ของการเกิดโรค ขอให้เฝ้าระวังผู้ป่วยรายใหม่ในพื้นที่อย่างใกล้ชิด และควบคุมค่าดัชนีลูกน้ำ (HI CI) ในหมู่บ้านไม่ให้เกิน 5% เพื่อตัดวงจรการระบาดรอบสอง

)} {/* Timeline */}

ไทม์ไลน์มาตรการ 3-3-1-7-14-21-28

{steps.map((step, index) => { const isActive = activePhase === step.id; const isComp = step.status === 'completed'; let btnStyle = "bg-white text-slate-300 border-slate-200 hover:border-slate-300"; let textStyle = "text-slate-400"; let dateStyle = "text-slate-400"; if (isComp) { btnStyle = "bg-emerald-50 text-emerald-600 border-emerald-300 shadow-sm"; textStyle = "text-emerald-700"; dateStyle = "text-emerald-600"; } else if (isActive) { btnStyle = "bg-indigo-50 text-indigo-600 border-indigo-500 shadow-md scale-110 ring-4 ring-indigo-50"; textStyle = "text-indigo-800 font-bold"; dateStyle = "text-indigo-600 font-semibold"; } const isStepOverdue = selectedPatient.daysSinceOnset > step.targetDays && !isComp && step.id !== 'summary'; if (isStepOverdue && !isActive && !isComp) { btnStyle = "bg-rose-50 text-rose-500 border-rose-300 shadow-sm"; textStyle = "text-rose-700"; dateStyle = "text-rose-600 font-semibold"; } let targetDateStr = '-'; if (selectedPatient?.onsetDateObj) { const tDate = new Date(selectedPatient.onsetDateObj); tDate.setDate(tDate.getDate() + step.targetDays); targetDateStr = formatThaiDate(tDate); } return (
{index !== steps.length - 1 &&
}

{step.label}

{targetDateStr}

)})}
{activePhase !== 'summary' && (

{activeStepObj?.label}

{isLocked && }
{isPhaseOverdue && (
กิจกรรมนี้เกินกำหนดเวลาแล้ว กรุณารีบดำเนินการ
)} {suggestionText && (

แนวทางปฏิบัติตามมาตรฐานกระทรวงสาธารณสุข:

{suggestionText}

)}
ส่งรูปเข้า Google Form แนบภาพกิจกรรมสำหรับระยะ {activeStepObj?.label}
ตรวจสอบฐานข้อมูลภาพ ดูข้อมูลใน Google Sheets แบบเรียลไทม์

คำแนะนำ: ในการอัปโหลดรูปผ่าน Google Form กรุณาพิมพ์ รหัสผู้ป่วย (HN) ให้ถูกต้อง

{activePhase === '3h1' && (
)} {activePhase === '3h2' && (

รายการตรวจสอบการปฏิบัติงาน

{renderLarvaeSurveyForm(isLocked, "สำรวจข้อมูลพื้นฐาน Day 0")}
)} {(activePhase === '1d' || activePhase === '3d' || activePhase === '7d') && (

เงื่อนไขปฏิบัติการเพิ่มเติม (เลือกถ้ามี)

{renderLarvaeSurveyForm(isLocked, "เป้าหมายการควบคุม HI รัศมี 100ม. = 0%")}
)} {(activePhase === '14d' || activePhase === '21d') && (
{renderLarvaeSurveyForm(isLocked, activePhase === '14d' ? "เป้าหมาย HI หมู่บ้าน ≤ 5%" : "เป้าหมาย HI ≤ 5%, CI สถานพยาบาล/โรงเรียน = 0%, CI อื่นๆ ≤ 5%")}
)} {activePhase === '28d' && (
)} {!isLocked && ( )}
)} {/* 🌟 ส่วนที่เพิ่มใหม่: สรุปกิจกรรมภาพรวมของผู้ป่วย (Summary View) */} {activePhase === 'summary' && (() => { // เตรียมข้อมูลเพื่อนำมาสรุป const d0_1 = formData['3h1'] || {}; const d0_2 = formData['3h2'] || {}; const d1 = formData['1d'] || {}; const d3 = formData['3d'] || {}; const d7 = formData['7d'] || {}; const d14 = formData['14d'] || {}; const d21 = formData['21d'] || {}; const d28 = formData['28d'] || {}; const getHiStr = (found, total) => total ? `${calcPercent(found, total)}% (${found}/${total})` : '-'; const getCiStr = (found, total, name) => total ? `${calcPercent(found, total)}% ${name ? `(${name})` : ''}` : '-'; return (

สรุปกิจกรรมภาพรวมของผู้ป่วย

ข้อมูลเบื้องต้นในการเขียนแบบสอบสวนโรค (รวมผลจาก Day 0 - Day 28)

{/* 1. ข้อมูลผู้ป่วยและพื้นที่ */}

1. ข้อมูลผู้ป่วยและพื้นที่รับผิดชอบ

รหัสผู้ป่วย (HN) / อายุ{selectedPatient.displayHn} (อายุ {selectedPatient.age} ปี)
พื้นที่เกิดโรคหมู่บ้าน{selectedPatient.village} ต.{selectedPatient.subdistrict}
วันที่เริ่มป่วย (Onset Date){selectedPatient.date}
วันที่รับแจ้งโรค (Report Date){d0_1.reportTime ? formatDateTimeThai(d0_1.reportTime) : 'ยังไม่ได้ระบุ'}
{/* 2. การตอบโต้ Day 0 */}

2. การตอบโต้ระยะฉับพลัน (Day 0)

วันที่ลงพื้นที่ฉุกเฉิน{d0_2.visitDate ? formatDateTimeThai(d0_2.visitDate) : 'ยังไม่ได้ระบุ'}
ผู้ปฏิบัติงาน{d0_2.visitorName || '-'}
{d0_2.isSprayed ? : } ฉีดสเปรย์ฆ่ายุง
{d0_2.giveRepellent ? : } แจกยาทากันยุง
{/* 3. การพ่นเคมี */}

3. มาตรการพ่นสารเคมีกำจัดยุงตัวแก่ (Day 1, 3, 7)

รอบการพ่น วันที่ดำเนินการ สถานะพ่น 100ม. เงื่อนไขพิเศษที่ดำเนินการ
ครั้งที่ 1 (Day 1) {d1.visitDate ? formatThaiDate(d1.visitDate) : '-'} {d1.isFogging ? ดำเนินการแล้ว : '-'} {d1.sprayVillage && พ่นทั้งหมู่บ้าน} {d1.rainCampaign && รณรงค์หลังฝน} {d1.weeklySpray && พ่นเสริมรายสัปดาห์} {!d1.sprayVillage && !d1.rainCampaign && !d1.weeklySpray && '-'}
ครั้งที่ 2 (Day 3) {d3.visitDate ? formatThaiDate(d3.visitDate) : '-'} {d3.isFogging ? ดำเนินการแล้ว : '-'} {d3.sprayVillage && พ่นทั้งหมู่บ้าน} {d3.rainCampaign && รณรงค์หลังฝน} {d3.weeklySpray && พ่นเสริมรายสัปดาห์} {!d3.sprayVillage && !d3.rainCampaign && !d3.weeklySpray && '-'}
ครั้งที่ 3 (Day 7) {d7.visitDate ? formatThaiDate(d7.visitDate) : '-'} {d7.isFogging ? ดำเนินการแล้ว : '-'} {d7.sprayVillage && พ่นทั้งหมู่บ้าน} {d7.rainCampaign && รณรงค์หลังฝน} {d7.weeklySpray && พ่นเสริมรายสัปดาห์} {!d7.sprayVillage && !d7.rainCampaign && !d7.weeklySpray && '-'}
{/* 4. สถิติลูกน้ำ */}

4. ผลการสำรวจดัชนีลูกน้ำยุงลาย (HI / CI)

{[ { label: 'Day 0', data: d0_2 }, { label: 'Day 1', data: d1 }, { label: 'Day 3', data: d3 }, { label: 'Day 7', data: d7 }, { label: 'Day 14', data: d14 }, { label: 'Day 21', data: d21 }, ].map((row, i) => ( ))}
ระยะเวลา HI 100 เมตร HI ทั้งหมู่บ้าน CI สถานที่สำคัญ
{row.label} {getHiStr(row.data.survey100mFound, row.data.survey100mTotal)} {getHiStr(row.data.surveyCommFound, row.data.surveyCommTotal)}
{getCiStr(row.data.schoolFound, row.data.schoolTotal, row.data.schoolName)} {(row.data.otherPlaceTotal) && {getCiStr(row.data.otherPlaceFound, row.data.otherPlaceTotal, row.data.otherPlaceName)}}

บ้านเลขที่ที่พบลูกน้ำ

{[d0_2, d1, d3, d7, d14, d21].map(d => d.housesFound100m).filter(Boolean).join(', ') || '- ไม่พบข้อมูล/ไม่ได้ระบุ -'}

{/* 5. การประเมิน 28 วัน */}

5. การเฝ้าระวังผู้ป่วยใหม่และประเมินผล 28 วัน

สถานะผู้ป่วย Gen 2 (Day 14-21)

{d14.checkGen2 || d21.checkGen2 ? สำรวจแล้วไม่พบผู้ป่วยรายใหม่ : รอผลการเฝ้าระวัง}

สถานะยุติการระบาด (Day 28)

{d28.isCaseClosed ? ไม่พบเคสใหม่เกิน 28 วัน (ปิดเคส) : รอผลประเมิน Day 28}

บันทึกถอดบทเรียน (AAR)

{d28.aarNote || '- ยังไม่มีการบันทึกข้อความถอดบทเรียน -'}

); })()}
); }; const renderAnalytics = () => { const getStatus = (valStr, den, type) => { if (den === 0) return 'nodata'; const val = parseFloat(valStr); if (type === 'max100') return val >= 100 ? 'pass' : 'fail'; if (type === 'min90') return val >= 90 ? 'pass' : 'fail'; if (type === 'less20') return val <= 20 ? 'pass' : 'fail'; if (type === 'less5') return val < 5 ? 'pass' : 'fail'; return 'nodata'; }; // 🌟 นำ Response Time ออกและเหลือเพียง 4 KPI ตามที่ผู้ใช้กำหนด const kpiDefinitions = [ { id: 1, title: "1. ติดตามผู้ป่วยครบ 28 วัน (1,3,7,28)", value: kpiStats.kpi2.val, num: kpiStats.kpi2.num, den: kpiStats.kpi2.den, unit: "ราย", target: "เป้าหมาย 100% (ลงข้อมูลครบถ้วน)", color: "emerald", type: "max100", methodology: "คำนวณจาก: ผู้ป่วยที่เลย 28 วันไปแล้ว ต้องมีการประเมินและบันทึกข้อมูลในระยะ Day 1, Day 3, Day 7 และ Day 28 ครบทั้ง 4 ระยะ" }, { id: 2, title: "2. ไม่พบผู้ป่วยรายใหม่ใน 28 วัน", value: kpiStats.kpi3.val, num: kpiStats.kpi3.num, den: kpiStats.kpi3.den, unit: "ราย", target: "เป้าหมาย ≥ 90% (ยุติการระบาด)", color: "rose", type: "min90", methodology: "คำนวณจาก: ผู้ป่วยตั้งต้นที่ไม่ทำให้เกิดผู้ป่วยรายใหม่ (ในหมู่บ้านเดียวกัน) ภายในช่วง 1 - 28 วันถัดมา" }, { id: 3, title: "3. อัตราเกิดผู้ป่วย Day 15-28 (Gen 2)", value: kpiStats.kpi4.val, num: kpiStats.kpi4.num, den: kpiStats.kpi4.den, unit: "ราย", target: "เป้าหมาย ≤ 20% (ควบคุมได้ > 80%)", color: "amber", type: "less20", methodology: "คำนวณจาก: จำนวนผู้ป่วยใหม่ที่พบในช่วงวันที่ 15 - 28 (นับเป็น Generation 2 ที่เกิดจากการควบคุมแหล่งเพาะพันธุ์ล้มเหลว)" }, { id: 4, title: "4. อัตราพบลูกน้ำซ้ำซาก (Day 0,1,3,7)", value: kpiStats.kpi5.val, num: kpiStats.kpi5.num, den: kpiStats.kpi5.den, unit: "พื้นที่", target: "เป้าหมาย < 5% (บ้านเลขที่เดิม)", color: "purple", type: "less5", methodology: "คำนวณจาก: การพบลูกน้ำใน 'บ้านเลขที่เดิม' ซ้ำซากจากการลงสำรวจใน 4 ระยะแรก (Day 0, Day 1, Day 3 และ Day 7)" } ]; const colorClasses = { blue: 'border-l-blue-500 text-blue-700 bg-blue-500', emerald: 'border-l-emerald-500 text-emerald-700 bg-emerald-500', rose: 'border-l-rose-500 text-rose-700 bg-rose-500', amber: 'border-l-amber-500 text-amber-700 bg-amber-500', purple: 'border-l-purple-500 text-purple-700 bg-purple-500' }; return (

แดชบอร์ดประเมินผลตัวชี้วัด (KPI)

ประมวลผลตัวชี้วัดความสำเร็จอัตโนมัติ จากฐานข้อมูลการลงพื้นที่จริง

ข้อมูลปี พ.ศ. {selectedYear === 'all' || selectedYear === 'latest' ? availableYears[0] : selectedYear}
{kpiDefinitions.map(kpi => { const isExpanded = expandedKpi === kpi.id; const cClass = colorClasses[kpi.color]; const borderClass = cClass.split(' ')[0]; const textClass = cClass.split(' ')[1]; const bgClass = cClass.split(' ')[2]; return (
{kpi.status === 'pass' && ผ่านเกณฑ์} {kpi.status === 'fail' && ไม่ผ่านเกณฑ์} {kpi.status === 'nodata' && ไม่มีข้อมูล}

{kpi.title}

{kpi.value} %
ผลงาน
{kpi.num} {kpi.unit}
เป้าหมาย
{kpi.den} {kpi.unit}

{kpi.target}

{isExpanded && (

วิธีการคำนวณและประเมินผล

{kpi.methodology}
)}
); })}
); }; return (
{/* Mobile Menu Overlay */} {isMobileMenuOpen && (
setIsMobileMenuOpen(false)} >
)} {/* Sidebar */}

ระบบเฝ้าระวังและควบคุมโรคไข้เลือดออกอัจฉริยะ (อ.พนา) Smart Dengue (อ.พนา)

Smart EOC Dengue
{formatThaiDate(new Date())}
{activeMenu === 'patients' && renderPatientList()} {activeMenu === 'map' && renderMap()} {activeMenu === 'tracking' && renderTrackingView()} {activeMenu === 'analytics' && renderAnalytics()}
); }; export default App;